﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM
{
    #region Using Statements

    using System;
    using System.Linq;
    using System.Text;
    using System.Reflection;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using KLF.Lib.Browser.DOM.HTML;
    using KLF.Lib.Browser.DOM.Parser;

    #endregion

    public class Element : Node
    {
        #region Properties

        public List<Node> Attributes { get; protected set; }

        private StringLookup _TagName; // as original
        private StringLookup _tagName; // as all lower case
        private StringLookup _TAGName; // as all upper case

        /// <summary>
        /// 
        /// </summary>
        public string TagName
        {
            get
            {
                return _TagName.Value;
            }
            internal set
            {
                _TagName = new StringLookup(value);
                _TAGName = new StringLookup(value.ToUpperInvariant());
                _tagName = new StringLookup(value.ToLowerInvariant());
            }
        }

        public string tagName
        {
            get
            {
                return _tagName.Value;
            }
        }

        public string TAGName
        {
            get
            {
                return _TAGName.Value;
            }
        }

        public override string ToString()
        {
            return String.Format("Element[{0}]", tagName);
        }

        #endregion

        #region Constructors

        internal Element(Document owner, string tagName)
            : base(owner)
        {
            TagName = tagName;
            Attributes = new List<Node>();
        }

        internal Element(Element source)
            : base(source.Owner)
        {
            TagName = source.TagName;
            Attributes = new List<Node>();

            foreach (Attr att in source.Attributes)
            {
                Attributes.Add(new Attr(att));
            }
        }

        #endregion

        #region Non-Standard DOM Methods

        /// <summary>
        /// Compare attribute value (case sensitive).
        /// </summary>
        /// <param name="attrName"></param>
        /// <param name="attrValue"></param>
        /// <returns></returns>
        public bool eqAttribute(string attrName, string attrValue, bool ignoreCase)
        {
            if (ignoreCase)
                return eqAttribute(attrName, attrValue);
            else
                return eqiAttribute(attrName, attrValue);

        }

        /// <summary>
        /// Compare attribute value (case sensitive).
        /// </summary>
        /// <param name="attrName"></param>
        /// <param name="attrValue"></param>
        /// <returns></returns>
        public bool eqAttribute(string attrName, string attrValue)
        {
            return getAttribute(attrName)
                .Equals(attrValue);
        }

        /// <summary>
        /// Compare attribute value (non case-sensitive).
        /// </summary>
        /// <param name="attrName"></param>
        /// <param name="attrValue"></param>
        /// <returns></returns>
        public bool eqiAttribute(string attrName, string attrValue)
        {
            return getAttribute(attrName)
                .ToLowerInvariant()
                .Equals(attrValue.ToLowerInvariant());
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        public T getAttributeAsType<T>(string name, T defaultValue)
        {
            String rawValue = getAttribute(name);

            try
            {
                if (String.IsNullOrEmpty(rawValue.Trim()))
                    return defaultValue;

                return (T)Convert.ChangeType(rawValue, typeof(T));
            }
            catch (Exception)
            {
                try
                {
                    Type type = typeof(T);

                    ConstructorInfo con = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
                        .Where(
                            c => c.GetParameters().Count() == 1 &&
                            c.GetParameters().First().ParameterType == typeof(String))
                        .First();

                    return (T)con.Invoke(new object[] { rawValue });
                }
                catch (Exception)
                {
                    return default(T);
                }
            }
        }

        public virtual string outerHTML
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                sb.Append('<');
                sb.Append(TagName);
                foreach (Attr att in Attributes)
                {
                    sb.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
                }

                if (Children.Count == 0)
                {
                    sb.Append(" />");
                }
                else
                {
                    sb.Append('>');

                    foreach (Node n in Children)
                    {
                        if (n is Text)
                            sb.Append(((Text)n).outerHTML);
                        else if (n is HTMLElement)
                            sb.Append(((HTMLElement)n).outerHTML);
                    }
                    sb.AppendFormat("</{0}>", TagName);
                }

                return sb.ToString();
            }
            set
            {
                if (Parent != null)
                {
                    DocumentFragment frag = Owner.createDocumentFragment();
                    DocumentBuilder.ParseFragment(value, Owner, frag);

                    Parent.replaceChild(frag, this);
                }
            }
        }

        internal string GetIndentedHtml(int depth)
        {
            StringBuilder sb = new StringBuilder();

            if (depth > 0)
                sb.Append(new String('\t', depth));
            sb.Append('<');
            sb.Append(TagName);
            foreach (Attr att in Attributes)
            {
                if (att.name == HTMLConstants.HTML_ATT_DEFAULT_CHECKED ||
                    att.name == HTMLConstants.HTML_ATT_DEFAULT_SELECTED ||
                    att.name == HTMLConstants.HTML_ATT_DEFAULT_VALUE)
                    continue;
                sb.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
            }

            if (Children.Count == 0)
            {
                sb.Append(" />\n");
            }
            else
            {
                sb.Append(">\n");

                foreach (Node n in Children)
                {
                    if (n is Text)
                    {
                        string s = ((Text)n).outerHTML;

                        if (s.Trim().Length > 0)
                        {
                            sb.Append(new String('\t', depth + 1));
                            sb.Append(s);

                            if (!s.EndsWith("\n"))
                                sb.Append('\n');
                        }
                    }
                    else if (n is HTMLElement)
                        sb.Append(((HTMLElement)n).GetIndentedHtml(depth + 1));
                }
                if (depth > 0)
                    sb.Append(new String('\t', depth));
                sb.AppendFormat("</{0}>\n", TagName);
            }

            return sb.ToString();
        }

        public void setAttributeAsType<T>(string name, T value)
        {
            setAttribute(name, value.ToString());
        }

        #endregion

        #region DOM Level 1 Compliance

        /// <summary>
        /// A NamedNodeMap containing the attributes of this node 
        /// (if it is an Element) or null otherwise. 
        /// </summary>
        public override NamedNodeMap attributes
        {
            get
            {
                return new NamedNodeMap(Attributes);
            }
        }

        /// <summary>
        /// Returns a duplicate of this node, i.e., serves as a generic 
        /// copy constructor for nodes. The duplicate node has no parent
        /// (parentNode returns null.).
        /// 
        /// Cloning an Element copies all attributes and their values, 
        /// including those generated by the XML processor to represent 
        /// defaulted attributes, but this method does not copy any text 
        /// it contains unless it is a deep clone, since the text is 
        /// contained in a child Text node. Cloning any other type of node
        /// simply returns a copy of this node. 
        /// </summary>
        /// <param name="deep"></param>
        /// <returns></returns>
        public override Node cloneNode(bool deep)
        {
            Element clone = new Element(this);

            foreach (Attr att in Attributes)
            {
                clone.setAttributeNode(new Attr(att));
            }
            if (deep)
            {
                foreach (Node n in Children)
                    clone.Children.Add(n.cloneNode(deep));
            }

            return clone;
        }

        /// <summary>
        /// Retrieves an attribute value by name. 
        /// </summary>
        /// <param name="name">The name of the attribute to retrieve.</param>
        /// <returns>The Attr value as a string, or the empty string if that 
        /// attribute does not have a specified or default value.</returns>
        public virtual string getAttribute(string attName)
        {
            Attr att = getAttributeNode(attName);

            return (att != null ? att.Value : String.Empty);
        }

        /// <summary>
        /// Retrieves an Attr node by name. 
        /// </summary>
        /// <param name="name">The name of the attribute to retrieve.</param>
        /// <returns></returns>
        public Attr getAttributeNode(string name)
        {
            var query = Attributes.Where(a => ((Attr)a).Name.Equals(name));

            return (Attr)(query.Count() > 0 ? query.First() : null);
        }

        /// <summary>
        /// Returns a NodeList of all descendant elements with a given tag 
        /// name, in the order in which they would be encountered in a 
        /// preorder traversal of the Element tree. 
        /// </summary>
        /// <param name="name">The name of the tag to match on. 
        /// The special value "*" matches all tags.</param>
        /// <returns>A list of matching Element nodes</returns>
        public NodeList getElementsByTagName(string name)
        {
            var result = new NodeList(Elements
                .Where(e => {
                    if (name == "*" || e.TagName == name)
                        return true;
                    return false;
                })
                .Cast<Node>());
            return result;
        }

        /// <summary>
        /// The name of this node, depending on its type. 
        /// </summary>
        public override string nodeName { get { return TagName; } }

        /// <summary>
        /// A code representing the type of the underlying object.
        /// </summary>
        public override byte nodeType { get { return ELEMENT_NODE; } }

        /// <summary>
        /// The value of this node, depending on its type.
        /// </summary>
        public override string nodeValue { get { return null; } }

        /// <summary>
        /// Removes an attribute by name. If the removed attribute has a 
        /// default value it is immediately replaced. 
        /// </summary>
        /// <param name="name"></param>
        public virtual void removeAttribute(string name)
        {
            Attr node = getAttributeNode(name);

            if (node != null)
                Attributes.Remove(node);
        }

        /// <summary>
        /// Removes the specified attribute. 
        /// </summary>
        /// <param name="oldAttr">The Attr node to remove from the attribute 
        /// list. If the removed Attr has a default value it is immediately 
        /// replaced.</param>
        public virtual Attr removeAttributeNode(Attr oldAttr)
        {
            Attributes.Remove(oldAttr);
            return oldAttr;
        }

        /// <summary>
        /// Adds a new attribute. If an attribute with that name is already
        /// present in the element, its value is changed to be that of the 
        /// value parameter. This value is a simple string, it is not parsed
        /// as it is being set. So any markup (such as syntax to be 
        /// recognized as an entity reference) is treated as literal text, 
        /// and needs to be appropriately escaped by the implementation when
        /// it is written out. In order to assign an attribute value that 
        /// contains entity references, the user must create an Attr node 
        /// plus any Text and EntityReference nodes, build the appropriate 
        /// subtree, and use setAttributeNode to assign it as the value of 
        /// an attribute. 
        /// </summary>
        /// <param name="name">The name of the attribute to create or alter.</param>
        /// <param name="value">Value to set in string form.</param>
        public virtual void setAttribute(string name, string value)
        {
            Attr att = getAttributeNode(name);

            if (att != null)
            {
                att.Value = value;
                att.ownerElement = this;
            }
            else
            {
                Attr newAttribute = Owner.createAttribute(name);
                newAttribute.Value = value;
                newAttribute.ownerElement = this;
                setAttributeNode(newAttribute);
            }
        }

        /// <summary>
        /// Adds a new attribute. If an attribute with that name is already present in the element, 
        /// it is replaced by the new one. 
        /// </summary>
        /// <param name="newNode"></param>
        public virtual Attr setAttributeNode(Attr newNode)
        {
            Attr oldNode = getAttributeNode(newNode.Name);

            if (oldNode != null)
            {
                Attributes[Attributes.IndexOf(oldNode)] = newNode;
                return oldNode;
            }
            else
            {
                Attributes.Add(newNode);
                return null;
            }
        }

        #endregion

        #region DOM Level 2 Compliance

        /// <summary>
        /// Returns whether this node (if it is an element) has any attributes. 
        /// </summary>
        /// <returns>Returns true if this node has any attributes, false otherwise.</returns>
        public override bool hasAttributes()
        {
            return (Attributes.Count() > 0);
        }

        /// <summary>
        /// Returns an HTML
        /// </summary>
        public virtual string innerHTML
        {
            get
            {
                StringBuilder sb = new StringBuilder();

                foreach (Node n in Children)
                {
                    if (n is Text)
                        sb.Append((n as Text).outerHTML);
                    else if (n is HTMLElement)
                        sb.Append((n as HTMLElement).outerHTML);
                }

                return sb.ToString();
            }
            set
            {
                var list = new List<Node>(Children);
                foreach(Node n in list)
                {
                    removeChild(n);
                }
                DocumentBuilder.ParseFragment(value, Owner, this);
            }
        }

        /// <summary>
        /// Returns the local part of the qualified name of this node.
        /// 
        /// For nodes of any type other than ELEMENT_NODE and ATTRIBUTE_NODE
        /// and nodes created with a DOM Level 1 method, such as 
        /// Document.createElement(), this is always null.
        /// </summary>
        internal StringLookup _localName;
        public override string localName
        {
            internal set
            {
                _localName = new StringLookup(value);
            }
            get
            {
                return (_localName != null ? _localName.Value : null);
            }
        }

        /// <summary>
        /// The namespace URI of this node, or null if it is unspecified 
        /// (see XML Namespaces).
        /// 
        /// This is not a computed value that is the result of a namespace 
        /// lookup based on an examination of the namespace declarations in
        /// scope. It is merely the namespace URI given at creation time.
        /// 
        /// For nodes of any type other than ELEMENT_NODE and ATTRIBUTE_NODE
        /// and nodes created with a DOM Level 1 method, such as 
        /// Document.createElement(), this is always null.
        /// 
        /// Note: Per the Namespaces in XML Specification [XML Namespaces] 
        /// an attribute does not inherit its namespace from the element it
        /// is attached to. If an attribute is not explicitly given a 
        /// namespace, it simply has no namespace.
        /// </summary>
        internal StringLookup _namespaceURI;
        public override string namespaceURI
        {
            internal set
            {
                _namespaceURI = new StringLookup(value);
            }
            get
            {
                return (_namespaceURI != null ? _namespaceURI.Value : null);
            }
        }

        /// <summary>
        /// The namespace prefix of this node, or null if it is unspecified. 
        /// When it is defined to be null, setting it has no effect, 
        /// including if the node is read-only.
        /// 
        /// Note that setting this attribute, when permitted, changes the 
        /// nodeName attribute, which holds the qualified name, as well as 
        /// the tagName and name attributes of the Element and Attr 
        /// interfaces, when applicable.
        /// 
        /// Setting the prefix to null makes it unspecified, setting it to an
        /// empty string is implementation dependent.
        /// 
        /// Note also that changing the prefix of an attribute that is known 
        /// to have a default value, does not make a new attribute with the 
        /// default value and the original prefix appear, since the 
        /// namespaceURI and localName do not change.
        /// 
        /// For nodes of any type other than ELEMENT_NODE and ATTRIBUTE_NODE 
        /// and nodes created with a DOM Level 1 method, such as createElement
        /// from the Document interface, this is always null.
        /// </summary>
        internal StringLookup _prefix;
        public override string prefix
        {
            get
            {
                return (_prefix != null ? _prefix.Value : null);
            }
            set
            {
                _prefix = new StringLookup(value);
            }
        }

        #endregion
    }
}
